| 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328 |
2x
2x
2x
2x
2x
2x
169x
2x
359x
2x
1x
1x
1x
2x
2x
2x
432x
432x
432x
432x
432x
432x
432x
432x
432x
432x
430x
432x
432x
608x
608x
608x
608x
608x
608x
608x
2x
430x
430x
430x
2x
430x
430x
430x
430x
430x
2x
432x
432x
432x
432x
2x
2x
2x
2x
2x
2x
2x
2x
2x
2x
| /**
* Copyright 2017 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { User } from '../auth/user';
import { assert, fail } from '../util/assert';
import { Code, FirestoreError } from '../util/error';
import { FirebaseApp } from '@firebase/app-types';
import { _FirebaseApp } from '@firebase/app-types/private';
// TODO(mikelehen): This should be split into multiple files and probably
// moved to an auth/ folder to match other platforms.
export interface FirstPartyCredentialsSettings {
type: 'gapi';
client: Gapi;
sessionIndex: string;
}
export interface ProviderCredentialsSettings {
type: 'provider';
client: CredentialsProvider;
}
/** Settings for private credentials */
export type CredentialsSettings =
| FirstPartyCredentialsSettings
| ProviderCredentialsSettings;
export type TokenType = 'OAuth' | 'FirstParty';
export interface Token {
/** Type of token. */
type: TokenType;
/**
* The user with which the token is associated (used for persisting user
* state on disk, etc.).
*/
user: User;
/** Extra header values to be passed along with a request */
authHeaders: { [header: string]: string };
}
export class OAuthToken implements Token {
type = 'OAuth' as TokenType;
authHeaders: { [header: string]: string };
constructor(value: string, public user: User) {
this.authHeaders = { Authorization: `Bearer ${value}` };
}
}
/**
* A Listener for user change events.
*/
export type UserListener = (user: User) => void;
/**
* Provides methods for getting the uid and token for the current user and
* listening for changes.
*/
export interface CredentialsProvider {
/**
* Requests a token for the current user, optionally forcing a refreshed
* token to be fetched.
*/
getToken(forceRefresh: boolean): Promise<Token | null>;
/**
* Specifies a listener to be notified of user changes (sign-in / sign-out).
* It immediately called once with the initial user.
*/
setUserChangeListener(listener: UserListener): void;
/** Removes the previously-set user change listener. */
removeUserChangeListener(): void;
}
/** A CredentialsProvider that always yields an empty token. */
export class EmptyCredentialsProvider implements CredentialsProvider {
/**
* Stores the User listener registered with setUserChangeListener()
* This isn't actually necessary since the UID never changes, but we use this
* to verify the listen contract is adhered to in tests.
*/
private userListener: UserListener | null = null;
constructor() {}
getToken(forceRefresh: boolean): Promise<Token | null> {
return Promise.resolve<Token | null>(null);
}
setUserChangeListener(listener: UserListener): void {
assert(!this.userListener, 'Can only call setUserChangeListener() once.');
this.userListener = listener;
// Fire with initial user.
listener(User.UNAUTHENTICATED);
}
removeUserChangeListener(): void {
assert(
this.userListener !== null,
'removeUserChangeListener() when no listener registered'
);
this.userListener = null;
}
}
export class FirebaseCredentialsProvider implements CredentialsProvider {
/**
* The auth token listener registered with FirebaseApp, retained here so we
* can unregister it.
*/
private tokenListener: ((token: string | null) => void) | null = null;
/** Tracks the current User. */
private currentUser: User;
/**
* Counter used to detect if the user changed while a getToken request was
* outstanding.
*/
private userCounter = 0;
/** The User listener registered with setUserChangeListener(). */
private userListener: UserListener | null = null;
constructor(private readonly app: FirebaseApp) {
// We listen for token changes but all we really care about is knowing when
// the uid may have changed.
this.tokenListener = () => {
const newUser = this.getUser();
Eif (!this.currentUser || !newUser.isEqual(this.currentUser)) {
this.currentUser = newUser;
this.userCounter++;
if (this.userListener) {
this.userListener(this.currentUser);
}
}
};
this.userCounter = 0;
// Will fire at least once where we set this.currentUser
(this.app as _FirebaseApp).INTERNAL.addAuthTokenListener(
this.tokenListener
);
}
getToken(forceRefresh: boolean): Promise<Token | null> {
assert(
this.tokenListener != null,
'getToken cannot be called after listener removed.'
);
// Take note of the current value of the userCounter so that this method can
// fail (with an ABORTED error) if there is a user change while the request
// is outstanding.
const initialUserCounter = this.userCounter;
return (this.app as _FirebaseApp).INTERNAL.getToken(forceRefresh).then(
tokenData => {
// Cancel the request since the user changed while the request was
// outstanding so the response is likely for a previous user (which
// user, we can't be sure).
Iif (this.userCounter !== initialUserCounter) {
throw new FirestoreError(
Code.ABORTED,
'getToken aborted due to uid change.'
);
} else {
Iif (tokenData) {
assert(
typeof tokenData.accessToken === 'string',
'Invalid tokenData returned from getToken():' + tokenData
);
return new OAuthToken(tokenData.accessToken, this.currentUser);
} else {
return null;
}
}
}
);
}
setUserChangeListener(listener: UserListener): void {
assert(!this.userListener, 'Can only call setUserChangeListener() once.');
this.userListener = listener;
// Fire the initial event, but only if we received the initial user
Iif (this.currentUser) {
listener(this.currentUser);
}
}
removeUserChangeListener(): void {
assert(
this.tokenListener != null,
'removeUserChangeListener() called twice'
);
assert(
this.userListener !== null,
'removeUserChangeListener() called when no listener registered'
);
(this.app as _FirebaseApp).INTERNAL.removeAuthTokenListener(
this.tokenListener!
);
this.tokenListener = null;
this.userListener = null;
}
private getUser(): User {
// TODO(mikelehen): Remove this check once we're shipping with firebase.js.
Iif (typeof (this.app as _FirebaseApp).INTERNAL.getUid !== 'function') {
fail(
'This version of the Firestore SDK requires at least version' +
' 3.7.0 of firebase.js.'
);
}
const currentUid = (this.app as _FirebaseApp).INTERNAL.getUid();
assert(
currentUid === null || typeof currentUid === 'string',
'Received invalid UID: ' + currentUid
);
return new User(currentUid);
}
}
// TODO(b/32935141): Ideally gapi type would be declared as an extern
// tslint:disable-next-line:no-any
export type Gapi = any;
/*
* FirstPartyToken provides a fresh token each time its value
* is requested, because if the token is too old, requests will be rejected.
* TODO(b/33147818) this implementation violates the current assumption that
* tokens are immutable. We need to either revisit this assumption or come
* up with some way for FPA to use the listen/unlisten interface.
*/
export class FirstPartyToken implements Token {
type = 'FirstParty' as TokenType;
user = User.FIRST_PARTY;
constructor(private gapi: Gapi, private sessionIndex: string) {
assert(
this.gapi &&
this.gapi['auth'] &&
this.gapi['auth']['getAuthHeaderValueForFirstParty'],
'unexpected gapi interface'
);
}
get authHeaders(): { [header: string]: string } {
return {
Authorization: this.gapi['auth']['getAuthHeaderValueForFirstParty']([]),
'X-Goog-AuthUser': this.sessionIndex
};
}
}
/*
* Provides user credentials required for the Firestore JavaScript SDK
* to authenticate the user, using technique that is only available
* to applications hosted by Google.
*/
export class FirstPartyCredentialsProvider implements CredentialsProvider {
constructor(private gapi: Gapi, private sessionIndex: string) {
assert(
this.gapi &&
this.gapi['auth'] &&
this.gapi['auth']['getAuthHeaderValueForFirstParty'],
'unexpected gapi interface'
);
}
getToken(forceRefresh: boolean): Promise<Token | null> {
return Promise.resolve(new FirstPartyToken(this.gapi, this.sessionIndex));
}
// TODO(33108925): can someone switch users w/o a page refresh?
// TODO(33110621): need to understand token/session lifecycle
setUserChangeListener(listener: UserListener): void {
// Fire with initial uid.
listener(User.FIRST_PARTY);
}
removeUserChangeListener(): void {}
}
/**
* Builds a CredentialsProvider depending on the type of
* the credentials passed in.
*/
export function makeCredentialsProvider(credentials?: CredentialsSettings) {
if (!credentials) {
return new EmptyCredentialsProvider();
}
switch (credentials.type) {
case 'gapi':
return new FirstPartyCredentialsProvider(
credentials.client,
credentials.sessionIndex || '0'
);
case 'provider':
return credentials.client;
default:
throw new FirestoreError(
Code.INVALID_ARGUMENT,
'makeCredentialsProvider failed due to invalid credential type'
);
}
}
|